package com.sixnimmt.app.server.game;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.sixnimmt.app.client.game.GameService;
import com.sixnimmt.app.server.game.domain.Game;
import com.sixnimmt.app.server.game.domain.Player;
import com.sixnimmt.app.server.game.domain.PlayerChoice;
import com.sixnimmt.app.shared.game.domain.Board;
import com.sixnimmt.app.shared.game.domain.Card;
import com.sixnimmt.app.shared.game.domain.PlayCardResult;
import com.sixnimmt.app.shared.game.exception.ChoiceAlreadyMadeException;
import com.sixnimmt.app.shared.game.exception.GameAlreadyStartedException;
import com.sixnimmt.app.shared.game.exception.InvalidPlayerException;
import com.sixnimmt.app.shared.game.exception.InvalidStackException;
import com.sixnimmt.app.shared.game.exception.NotInHandException;
import com.sixnimmt.app.shared.game.information.ServerInformation;
import com.sixnimmt.app.shared.game.notification.CardAddedNotification;
import com.sixnimmt.app.shared.game.notification.CardsTakenNotification;
import com.sixnimmt.app.shared.game.notification.GameNotification;
import com.sixnimmt.app.shared.game.notification.GameOverNotification;
import com.sixnimmt.app.shared.game.notification.HasChoosenCardNotification;
import com.sixnimmt.app.shared.game.notification.HaveToChooseCardNotification;
import com.sixnimmt.app.shared.game.notification.HaveToChooseStackNotification;
import com.sixnimmt.app.shared.game.notification.InitPlayerHandNotification;
import com.sixnimmt.app.shared.game.notification.PlayerAddedNotification;
import com.sixnimmt.app.shared.game.notification.RevealCardsNotification;
import com.sixnimmt.app.shared.game.notification.RoundOverNotification;
import com.sixnimmt.app.shared.game.notification.ServerRestartedNotification;
import com.sixnimmt.app.shared.game.notification.StackChangeNotification;

/**
 * The server side implementation of the RPC service.
 */
@SuppressWarnings("serial")
public class GameServiceImpl extends RemoteServiceServlet implements GameService {

	private static final Logger log = Logger.getLogger(GameServiceImpl.class.getName());

	private static Game game;
	private static Map<String, List<GameNotification>> notificationsByPlayer;

	public GameServiceImpl() {
		super();
		game = new Game();
		notificationsByPlayer = new HashMap<String, List<GameNotification>>();
	}

	public GameServiceImpl(Object delegate) {
		super(delegate);
		game = new Game();
		notificationsByPlayer = new HashMap<String, List<GameNotification>>();
	}

	@Override
	public List<GameNotification> retieveGameNotifications(String playerName) {

		List<GameNotification> notifications = new ArrayList<GameNotification>();
		List<GameNotification> notificationsInMemory = notificationsByPlayer.get(playerName);
		if (notificationsInMemory != null) {
			notifications.addAll(notificationsInMemory);
			for (GameNotification gameNotification : notifications) {
				notificationsInMemory.remove(gameNotification);
			}
		}

		return notifications;
	}

	@Override
	public void startGame(String playerName) throws InvalidPlayerException, GameAlreadyStartedException {
		log.info("Game started by  " + playerName);
		checkPlayerSession(playerName);

		if (game.isStarted()) {
			log.warning("Game already started");
			throw new GameAlreadyStartedException();
		}

		game.setStarted(true);

		startRound();
	}

	private void startRound() {
		log.info("New round");
		game.getDeck().init();
		for (int i = 0; i < Game.CARDS_BY_PLAYER; i++) {
			for (Player player : game.getPlayers().values()) {
				player.addCardInHand(game.getDeck().distribute());
			}
		}
		game.getBoard().init(
				new Card[] { game.getDeck().distribute(), game.getDeck().distribute(), game.getDeck().distribute(),
						game.getDeck().distribute() });

		StackChangeNotification[] stackInitNotifications = new StackChangeNotification[Board.STACK_NUMBER];
		for (int i = 0; i < Board.STACK_NUMBER; i++) {
			stackInitNotifications[i] = new StackChangeNotification(game.getBoard().getStacks()[i].getCards(), i);
		}

		for (Player player : game.getPlayers().values()) {
			List<Card> playerHand = new ArrayList<Card>();
			playerHand.addAll(player.getHand());
			storeNotification(player.getName(), new InitPlayerHandNotification(playerHand));

			storeNotification(player.getName(), stackInitNotifications);

			storeNotification(player.getName(), new HaveToChooseCardNotification());
		}
	}

	@Override
	public ServerInformation retrieveServerInformation() {
		if (game.isStarted())
			return ServerInformation.GAME_ALREADY_STARTED;
		if (game.getPlayers().size() >= Game.MAX_PLAYERS)
			return ServerInformation.SERVER_FULL;

		return ServerInformation.OK;
	}

	@Override
	public boolean joinGame(String playerName) {
		if (notificationsByPlayer.get(playerName) != null) {
			notificationsByPlayer.get(playerName).clear();
		}
		for (Player player : game.getPlayers().values()) {
			if (playerName.equals(player.getName())) {
				log.info("Player " + playerName + " already in-game");
				return false;
			}
		}

		for (Player oldPlayer : game.getPlayers().values()) {
			storeNotification(oldPlayer.getName(), new PlayerAddedNotification(playerName));
			storeNotification(playerName, new PlayerAddedNotification(oldPlayer.getName()));
		}

		Player newPlayer = new Player(playerName, getThreadLocalRequest().getSession().getId());
		game.getPlayers().put(playerName, newPlayer);

		log.info("Player " + playerName + " with session id " + newPlayer.getSessionId() + "has joined");

		return true;
	}

	private void storeNotification(String playerName, GameNotification... notifications) {
		List<GameNotification> playerNotifications = notificationsByPlayer.get(playerName);
		if (playerNotifications == null) {
			playerNotifications = new ArrayList<GameNotification>();
			notificationsByPlayer.put(playerName, playerNotifications);
		}

		for (int i = 0; i < notifications.length; i++) {
			playerNotifications.add(notifications[i]);
		}
	}

	@Override
	public void playCard(String playerName, Card card) throws NotInHandException, InvalidPlayerException,
			ChoiceAlreadyMadeException {
		checkPlayerSession(playerName);

		if (game.isChoiceMade(playerName)) {
			throw new ChoiceAlreadyMadeException();
		}

		Player player = game.getPlayers().get(playerName);
		PlayerChoice playerChoice = player.playCard(card);
		game.getPlayerChoices().add(playerChoice);

		for (Player otherPlayer : game.getPlayers().values()) {
			if (!otherPlayer.equals(player)) {
				storeNotification(otherPlayer.getName(), new HasChoosenCardNotification(player.getName()));
			}
		}

		if (game.getPlayerChoices().size() == game.getPlayers().size()) {
			Collections.sort(game.getPlayerChoices());

			revealCardToPlayers();

			performPlayerChoices();
		}

	}

	private void revealCardToPlayers() {
		Map<String, Card> choicesMap = new HashMap<String, Card>();
		for (PlayerChoice playerChoice : game.getPlayerChoices()) {
			choicesMap.put(playerChoice.getPlayer().getName(), playerChoice.getCard());
		}

		for (Player player : game.getPlayers().values()) {
			storeNotification(player.getName(), new RevealCardsNotification(choicesMap));
		}
	}

	@Override
	public void chooseStack(String playerName, int stack) throws InvalidStackException, InvalidPlayerException {
		checkPlayerSession(playerName);
		if (stack < 0 || stack > 3) {
			throw new InvalidStackException();
		}

		PlayerChoice playerChoice = game.getPlayerChoices().get(0);
		PlayCardResult result = game.getBoard().playCard(playerChoice.getCard(), stack);
		playerChoice.getPlayer().takeCards(result.getTakenCards());
		sendNotificationForCardPlayed(playerChoice, result);
		game.getPlayerChoices().remove(0);

		performPlayerChoices();

	}

	private void checkPlayerSession(String playerName) throws InvalidPlayerException {
		Player p = game.getPlayers().get(playerName);
		if (p == null) {
			log.warning("Player " + playerName + " is not in game");
			throw new InvalidPlayerException();
		} else if (!p.getSessionId().equals(getThreadLocalRequest().getSession().getId())) {
			log.warning("Player " + playerName + " is in game but with different session id (" + p.getSessionId()
					+ " != " + getThreadLocalRequest().getSession().getId() + ")");
			throw new InvalidPlayerException();
		}
	}

	private void performPlayerChoices() {
		while (!game.getPlayerChoices().isEmpty()) {
			PlayerChoice playerChoice = game.getPlayerChoices().get(0);

			if (playerChoice.getCard().getValue() < game.getBoard().getMinValueCardForAllStack().getValue()) {

				storeNotification(playerChoice.getPlayer().getName(), new HaveToChooseStackNotification());
				return;

			} else {

				PlayCardResult result = game.getBoard().playCard(playerChoice.getCard());
				playerChoice.getPlayer().takeCards(result.getTakenCards());
				sendNotificationForCardPlayed(playerChoice, result);

			}

			game.getPlayerChoices().remove(0);
		}

		Player aPlayer = game.getPlayers().values().iterator().next();
		if (aPlayer.getHand().isEmpty()) {
			Map<String, Integer> scores = new HashMap<String, Integer>();
			boolean gameIsOver = false;
			for (Player player : game.getPlayers().values()) {
				player.updateScore();
				player.clearCards();
				scores.put(player.getName(), player.getScore());
				if (player.getScore() >= Game.MAX_SCORE) {
					gameIsOver = true;
				}
			}
			RoundOverNotification roundOverNotification;
			if (gameIsOver) {
				roundOverNotification = new GameOverNotification(scores);
				log.info("Game over!");
			} else {
				roundOverNotification = new RoundOverNotification(scores);
				log.info("Round over!");
			}
			for (Player player : game.getPlayers().values()) {
				storeNotification(player.getName(), roundOverNotification);
			}
			if (!gameIsOver) {
				startRound();
			} else {
				log.info("Server restarted because game is over");
				restartServer();
			}
		} else {
			for (Player player : game.getPlayers().values()) {
				storeNotification(player.getName(), new HaveToChooseCardNotification());
			}
		}

	}

	private void restartServer() {
		// notificationsByPlayer.clear();
		game = new Game();
	}

	private void sendNotificationForCardPlayed(PlayerChoice playerChoice, PlayCardResult result) {
		for (Player player : game.getPlayers().values()) {

			if (!result.getTakenCards().isEmpty()) {
				storeNotification(player.getName(),
						new StackChangeNotification(Collections.<Card> emptyList(), result.getStackIndex()));
				storeNotification(player.getName(), new CardsTakenNotification(playerChoice.getPlayer().getName(),
						playerChoice.getPlayer().getTakenCards().size()));
			}
			storeNotification(player.getName(), new CardAddedNotification(playerChoice.getPlayer().getName(),
					playerChoice.getCard(), result.getStackIndex(), result.getPosition()));
		}
	}

	@Override
	public void restartServer(String playerName) throws InvalidPlayerException {
		checkPlayerSession(playerName);

		for (Player player : game.getPlayers().values()) {
			storeNotification(player.getName(), new ServerRestartedNotification());
		}

		log.info("Server restarted by user " + playerName);
		restartServer();
	}
}
